#!/usr/bin/env python

from amgineirc.model.amgine.parts.spinner import Spinner

import random

class Spin(object):
    """
        reads a single atom from istream.
        that atom is used as a spinner key.
        then it lets the spinner convert the key to a value.
        its stores the values in a list of values.
        the rule may limit the lengh of a list of values.
        if so then another list of values is started.
        returns a list of the list of values.
    """
    def __init__(self, rule, istream):
        self._rule = rule
        self._i_stream = istream
        self._build_spinners()
        self._spinner_index = 0
        self._no_atoms_in_value = 0
        self._value_lists = []
        self._value_list_index = -1

    def spin(self):
        ok = self._build_new_value_list()
        while ok:
            ok = self._build_new_value_list()
        # return a copy of the 
        r_list = [i[:] for i in self._value_lists]
        return r_list

    def _init_spinners(self):
        self._spinner_index = 0
        # reset the spinners for a new shift
        for spinner in self._spinners:
            spinner.reset()

    def _build_new_value_list(self):
        """reads keys and builds a list of values.
        returns True if not eof.
        """
        if self._i_stream.eof:
            return False
        else:
            # there is more to input
            # initialize the spinners
            self._init_spinners()
            # create and write the header
            #   shift the spinners
            #   set the starting spinner
            header = self._do_header()
            # read the line of text converting each char to values
            self._start_new_value_list()
            self._no_atoms_in_value = 0
            self._write_value(header)
            v = self._read_key_convert_to_value()
            while v is not None:
                self._write_value(v)
                if self._must_end_code_now() is True:
                    break
                v = self._read_key_convert_to_value()
            self._do_footer()
            return True

    def _must_end_code_now(self):
        k = self._i_stream.peek_one()
        if k is None:
            # no more input so stop
            return True
        else:
            # there is more input
            if self._rule.spin_length <= 0:
                # no length is specified
                # so do not stop
                return False
            else:
                # a length is specified
                # can we add the next value string from the spinner
                # without exceeding the length?
                value_len = self._spinners[self._spinner_index].get_value_length()
                return ( self._no_atoms_in_value + value_len ) > self._rule.spin_length

    def _do_header(self):
        # skip over header noise
        noise = []
        noise_len = self._rule.spin_noise_count
        v = self._spinners[0].peek_random_value()
        is_str = isinstance(v, str)
        while noise_len > 0:
            v = self._spinners[0].peek_random_value()
            if is_str:
                if len(v) <= noise_len:
                    noise.append(v)
                    noise_len -= len(v)
                else:
                    noise.append(v[0:noise_len])
                    noise_len= 0
            else:
                noise.append(v)
                noise_len -= 1
        # header shift
        (index, value) = self._spinners[0].peek_random_index_value(self._rule.eot_key)
         # shift the spinners
        if index > 0:
            for spinner in self._spinners:
                spinner.shift(index)
        noise.append(value)
        # random start spinner
        (index, value) = self._spinners[0].peek_random_index_value(self._rule.eot_key)
        self._spinner_index = index % len(self._spinners) 
        noise.append(value)
        if is_str:
            return "".join(noise)
        else:
            return noise

    def _do_footer(self):
        if self._rule.spin_length > 0:
            # add end of file
            value = self._spinners[self._spinner_index].get_value(self._rule.eot_key)
            self._write_value(value)
            self._inc_spinner_index()
            if self._no_atoms_in_value < self._rule.spin_length: 
                rlen =  random.randint(self._no_atoms_in_value, self._rule.spin_length) 
                while self._no_atoms_in_value < rlen:
                    # pad the end of the file
                    value = self._spinners[self._spinner_index].peek_random_value()
                    self._inc_spinner_index()
                    # write the random value on char at a time
                    v_list = list(value)
                    for v in v_list:
                        self._write_value(v)
                        if self._no_atoms_in_value == rlen:
                            break

    def _build_spinners(self):
        """build spinners according to the rules"""
        self._spinners = []
        len_k = len(self._rule.keys)
        len_v = len(self._rule.values)
        if len_k == len_v:
            for i in range(0, len_k, 1):
                if len(self._rule.keys[i]) == len(self._rule.values[i]):
                    self._spinners.append(
                        Spinner(self._rule.keys[i],
                                self._rule.values[i],
                                self._rule.shifts[i])
                        )

    def _read_key_convert_to_value(self):
        # a key is always a single atom
        # a value may be a string or single atom
        k = self._i_stream.read_one()
        if k is not None:
                v = self._spinners[self._spinner_index].get_value(k)
                if v is None:
                    raise LookupError("Spin Lookup Error: Unable to find the byte {0} in the current spinner.".format(k))
                else:
                    # this spinner was used so inc the index
                    self._inc_spinner_index()
                    return v
        return None
    
    def _start_new_value_list(self):
        self._value_lists.append([])
        self._value_list_index = len(self._value_lists) - 1

    def _write_value(self, v):
        if isinstance(v, str):
            self._no_atoms_in_value += len(v)
            self._value_lists[self._value_list_index].append(v)
        elif isinstance(v, list):
            self._no_atoms_in_value += len(v)
            for i in v:
                self._value_lists[self._value_list_index].append(i)
        else:
            # byte
            self._no_atoms_in_value += 1
            self._value_lists[self._value_list_index].append(v)

    def _inc_spinner_index(self):
        """don't use a spinner twice in a row"""
        self._spinner_index += 1
        if self._spinner_index == len(self._spinners):
            self._spinner_index = 0
